#include <cfloat>
#include <cmath>

#include <QDebug>
#include <QMouseEvent>

#include "setviewerpanel.h"
#include "datacontroller.h"

SetViewerPanel::SetViewerPanel(const Collection *map, QWidget *parent) :
    QWidget(parent),
    m_collection(map),
    m_held_set(NoSelection),
    m_sets_point(NoSelection),
    m_held_line(false)
{
    connect(this, SIGNAL(pointChanged()), this, SLOT(repaint()));
    connect(this, SIGNAL(setChanged()), this, SLOT(repaint()));
    connect(this, SIGNAL(lineChanged()), this, SLOT(repaint()));
}

void SetViewerPanel::paintEvent(QPaintEvent */*event*/) {

    /* Create a painter for the purpose of painting. */
    QPainter painter(this);

    /* Fill in the whole background of the fuzzy set in black */
    painter.fillRect(this->rect(), Qt::black);

    /* Get all required scaling factors */
    double xScale, xShift, yScale, yShift;
    this->getWidthFactors(xScale, xShift);
    this->getHeightFactors(yScale, yShift);

    /* Set colour to white, so the sets are visible on the black background */
    painter.setBrush(Qt::white);
    painter.setPen(Qt::white);

    /* Paint each of the sets in the fashion defined in that set */
    for ( int i = 0; i < m_collection->getSize(); ++i ) {
        /* If the set we're currently painting is the set that should be
         * highlighted, set the colour of the painter to red so that it is
         * painted differently than the other sets, paint it, then set the
         * painter back to white
         */
        if ( i == m_held_set ) {
            painter.setPen(Qt::red);
            m_collection->getSet(i)->paint(&painter, xShift, xScale,
                                           yShift, yScale);
            painter.setPen(Qt::white);
        }
        else
            m_collection->getSet(i)->paint(&painter, xShift, xScale,
                                           yShift, yScale);
    }

    /* Draw a green line at the displayed set's fuzzy value */
    double xPos;
    if ( m_collection->isOutput() ) {
        xPos = m_collection->getOutputValue();
    }
    else {
        xPos = m_collection->getInputValue();
    }

    painter.setPen(Qt::green);
    painter.drawLine(xShift + xScale * xPos, yShift + yScale,
                     xShift + xScale * xPos, yShift);


    /* Draw the scale on the bottom of each collection.*/
    painter.setPen(Qt::darkCyan);
    painter.drawLine(xShift + xScale * m_collection->getMinTerm(), yShift,
                     xShift + xScale * m_collection->getMaxTerm(), yShift);

    double range = m_collection->getMaxTerm() - m_collection->getMinTerm();

    /* Use 5 numbers for the scale.
     */
    for (int i=0; i < 5; ++i)

    {
        /* Calculate where each tick mark is to go on a centered scale.
         */
        double num = (m_collection->getMinTerm() + range/5.0*i + range/10.0);

        /*Set up the text to be a number with two decimal places.*/
        QString text;
        text.setNum(num, 'f', 2);

        painter.drawLine(xShift + xScale * num, yShift - yScale * 0.01,
                         xShift + xScale * num, yShift + yScale * 0.01);
        painter.drawText(xShift + xScale * num, yShift - yScale * 0.1,
                         text);
    }
}

/** This function "grabs" a point if it lies within a few pixels of the
    location where the mouse was pressed by saving the indices of the set and
    the point that are clicked.
 */
void SetViewerPanel::mousePressEvent(QMouseEvent *event) {

    /* Initialize the scaling variables */
    double xScale, xShift, yScale, yShift;
    this->getWidthFactors(xScale, xShift);
    this->getHeightFactors(yScale, yShift);

    /* Initialize the variable that can break the MF search, the distance for a
     * click to be registered and the point where the mouse was clicked
     */
    bool atLeastOneMF = false;

    const int SQ_DIST = 100;
    QPointF clickPos = event->posF();

    /* Initialize the variables that handle saving the nearest point */
    double nearestDistance = DBL_MAX;
    int savedSet;
    int savedPoint;

    /* Iterate over all of the sets of this viewer's Collection looking to see
     * if any are near the mouse.  If there is one, save the two indices so
     * that they can be updated appropriately later on.
     */
    Collection::Iterator a(*m_collection);

    while(a.hasNext())
    {
        a.next();

        QPointF pointPos(xShift + xScale * a.getPoint().x(),
                         yShift + yScale * a.getPoint().y());

        double distance = getSquareDist(clickPos, pointPos);

        if( distance < SQ_DIST && distance < nearestDistance ){

            /*If this was the first one found, save it.*/
            if(!atLeastOneMF){
                savedSet = a.getSetNumber();
                savedPoint = a.getPointNumber();
                atLeastOneMF = true;
            }

            /* For the second point found, if the centroid of the second point
             * found is closer to the clicked point than the centroid of the
             * membership function of the first point, change the saved
             * point, or else don't change the saved value.
             */
            else
            {

                double centroid1 = xShift +
                    xScale * m_collection->getSet(savedSet)->getCrispValue();
                double centroid2 = xShift +
                                   xScale * a.getSet()->getCrispValue();

                if(qAbs(clickPos.x()-centroid1) > qAbs(centroid2-clickPos.x()))
                {
                    savedSet = a.getSetNumber();
                    savedPoint = a.getPointNumber();
                }
            }
        }
        if (atLeastOneMF)
            grabPoint(savedSet, savedPoint);
    }
    /*If no points were selected, check if the crisp input line was selected*/
    if(!atLeastOneMF)
    {
        double xPos;
        if ( !m_collection->isOutput() ) {
            xPos = m_collection->getInputValue() * xScale + xShift;
            /* Use a distance of 10. It is fairly wide, and does not conflict
             * with the points due to the logic in the mouseMoveEvent.
             */
            if(clickPos.x() < xPos + 10 && clickPos.x() > xPos - 10)
                m_held_line = true;
        }
    }
}

/** This function releases the currently selected point that was being
 * modified.  It does not release the set.
 */
void SetViewerPanel::mouseReleaseEvent(QMouseEvent *) {
    m_sets_point = NoSelection;
    m_held_line = false;
}

void SetViewerPanel::mouseMoveEvent(QMouseEvent *event) {

    /* This function updates the point that is currently held if one exists.
       If one does not exist, check and see if the line is held
       and update that instead.
     */
    if (m_sets_point == NoSelection){
        if(m_held_line == false)
            return;
        else
        {
            double scale;
            double shift;
            this->getWidthFactors(scale, shift);


            double maxX = m_collection->getMaxTerm();
            double minX = m_collection->getMinTerm();

            double newPosition = (event->pos().x() - shift) / scale;

            /*prevent the line from going off the edge of the screen.*/
            if (newPosition <= maxX && newPosition >= minX)
            {
                double oldPosition = m_collection->getInputValue();

                if (newPosition != oldPosition)
                {
                    DataController::instance()->setFuzzyValue(m_collection,
                                                          newPosition);
                    emit lineChanged();
                }
            }
        }
        return;
    }

    double scale;
    double shift;
    this->getWidthFactors(scale, shift);

    double newPoint = (event->pos().x() - shift) / scale;

    double oldPoint = m_collection
                      ->getSet(m_held_set)
                      ->getPoints()[m_sets_point].x();

    /* Only emit the signal of a point having been changed if a point actually
     * has changed from its original value.
     */
    if ( newPoint != oldPoint ) {
        DataController::instance()->setPoint(m_collection, m_held_set,
                                             m_sets_point, newPoint);
        emit pointChanged();
    }

}

/** This function uses the current size of the fuzzy set viewer to get a
 * scaling factor and a shift factor in the x-direction.
 */
void SetViewerPanel::getWidthFactors(double& scale, double& shift) const {

    /* Need to get the minimum and maximum values that appear in the fuzzy set
     * that this viewer is responsible for, so that a scaling factor can be
     * generated.
     */
    double maxX = m_collection->getMaxTerm();
    double minX = m_collection->getMinTerm();

    /* The scaling factor will be the width of the viewer, divided by the range
     * of the values in the set.
     */
    scale = this->width() / (maxX - minX);

    /* The shifting factor is simply the negative of the lowest possible term
     * of the fuzzy sets.  If the value is negative, this ensures that the
     * viewer is shifted to the right, and if it is positive, the viewer is
     * shifted to the left.
     */
    shift = -minX * scale;

}

/** This function uses the current size of the fuzzy set viewer to get a
 * minimum and maximum y value for binding inside this widget.
 */
void SetViewerPanel::getHeightFactors(double& scale, double& shift) const {

    /* Sets the values so that the upper and lower values for the height view
     * of the set viewer take up 80% of the viewable height.  The values seem
     * strange because the y values must be flipped due to y-flippage of
     * computer screens.
     */
    scale = this->height() * -0.8;
    shift = this->height() * 0.9;
}

/** Grab a set and point */
void SetViewerPanel::grabPoint(const int &set, const int &point) {
    m_held_set = set;
    m_sets_point = point;
    emit setChanged();
}

/** Get the square distance between two points */
double SetViewerPanel::getSquareDist(const QPointF& a, const QPointF& b) const
{
    /* The distance between two points is the magnitude of the vector
     * difference between their displacement vectors.
     */
    double dx = a.x() - b.x();
    double dy = a.y() - b.y();

    /* We now have the displacement vector, but we need its magnitude to know
     * the square distance between the two points, so use pythagoras.
     */
    return dx * dx + dy * dy;
}

void SetViewerPanel::setDisplayedCollection(const Collection* newMap) {
    this->m_collection = newMap;
    m_held_set = NoSelection;
    m_sets_point = NoSelection;
    this->repaint();

}
